
昨天我們實戰 Ingress,理解了如何透過 Host-Based 與 Path-Based 路由,將外部流量智慧地導向 Cluster 內不同的 Service,解決了 NodePort 的侷限,並讓我們對 Kubernetes 南北向流量 (North-South Traffic) 管理有更清楚的認識。
不過,當服務之間的流量都解決了,我們得回到 Kubernetes 的最小部署單位 Pod 本身從 【Day03】Kubernetes 最小單位 Pod:不只是容器這麼簡單 開始,我們就知道一個 Pod 其實可以同時運行多個 Container。如果主應用程式啟動前需要一些前置檢查,或是需要輔助容器處理日誌與連線代理,就不能只靠單一容器完成。
查了一些 CKAD 考試心得後發現,Multi-Container Pod 幾乎是必考題,考點就是如何透過 Init Container 或 Sidecar 模式解決實務問題。所以,今天我們就來好好看看 Init Container 和 Multi-Container 的設計模式,並透過實戰來加深印象。
前幾天的實戰裡,我們一直維持「一個容器跑一個進程」的原則,這讓架構簡單、模組化。但實務上會遇到兩類難題:
對應的解法,就是在 Pod 中引入 Init Container 與 Sidecar / Adapter / Ambassador 這些設計模式。
Init Container 是為了解決「啟動前」的需求而設計。顧名思義,它是在 Pod 的主應用容器 (App Container) 啟動之前 運行的專用容器,其主要任務就是完成我們所設定的各種前置準備工作。它們的特點是:
restartPolicy 重啟,直到成功為止。這些特性,讓 Init Container 非常適合進行環境檢查與初始化工作。
對於需要「同步運行」的輔助任務,就要用 Multi-Container 模式,最常見的就是 Sidecar,還有 Adapter 和 Ambassador。
Sidecar 就像摩托車旁的邊車,主容器是摩托車,負責核心邏輯;而 Sidecar Container 則是邊車,負責提供輔助功能。它與主容器 並行運行,共享生命週期、網路和儲存。這種模式的強大之處在於,它可以在不侵入主應用的前提下,擴展或增強其功能,完美符合 職責分離 的原則。
一個常見的應用就是 日誌收集。假設有一個舊的應用程式,它習慣將 access log 寫入本地檔案 (例如 /var/log/app.log),而不是現代雲原生應用推薦的標準輸出 (stdout)。這會導致 kubectl logs 指令完全失效,因為它只能抓取 stdout 的內容。
Sidecar 的運作方式:
emptyDir Volumemain-app) 照常運行,將共享 Volume 掛載到它的日誌路徑 /var/log
busybox)同時運行,掛載同一個共享 Volumetail -f /var/log/app.log,持續讀取日誌並輸出到自己的 stdout如此一來,我們就可以透過 kubectl logs <pod-name> -c <sidecar-container-name> 間接看到主應用程式的日誌,完全不需要修改原本的應用程式。
Adapter 容器就像一個「翻譯官」,讓主應用程式可以繼續用自己最熟悉的方式輸出資訊,而由 Adapter 負責將這些資訊轉換成外界監控系統看得懂的標準格式。
假設主應用程式(例如 Java)輸出的監控指標是 JMX 格式,但外部的 Prometheus 監控系統只認得自己的標準格式。
Adapter 的運作方式:
java-app 容器照常運行,在 localhost 的某個 Port 上暴露 JMX 指標jmx_exporter (Adapter Container) 同時運行,存取 localhost 上 java-app 的 JMX Portjmx_exporter 將 JMX 格式指標轉換 (Adapt) 成 Prometheus 格式外部的 Prometheus 系統只需要抓取 Adapter Container 的 9113 Port 即可。
Ambassador 模式將與外部服務溝通的複雜邏輯從主應用中抽離出來。主應用程式只需要簡單地與 localhost 上的 Ambassador Container 通訊。
假設主應用程式需要連線到外部資料庫,而連線位址在不同環境(開發、生產)中都不同。如果把這些複雜的連線邏輯、重試機制、故障轉移都寫在主應用程式裡,程式碼會變得非常笨重且複雜。
Ambassador 的運作方式:
main-app 的資料庫連線位址寫死為 localhost:5432
ambassador Container 監聽 localhost:5432
main-app 嘗試連線時,實際上是連到同一 Pod 內的 ambassador Containerambassador 根據設定(透過環境變數或 ConfigMap),將請求轉發到真正的外部資料庫主應用程式所有對外的溝通都只需要跟 Ambassador Container 交談,完美地解除了主應用與外部環境的耦合。
首先,我們來看一個需要修改系統參數的例子。這模擬了某些資料庫(如 PostgreSQL、Elasticsearch)啟動前需要調整系統參數的場景:
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: ubuntu
  name: ubuntu
spec:
  replicas: 1
  selector:
    matchLabels:
      run: ubuntu
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: ubuntu
    spec:
      containers:
      - image: ubuntu
        name: ubuntu
        resources: {}
        command: ['sh', '-c', 'tail -f /dev/null']
      initContainers:
        - name: init-sysctl
          image: busybox
          command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
          securityContext:
            privileged: true
status: {}
執行後可以看到 Status 會顯示 Init Container 的進程與數量,Init Container 執行成功後,主應用程式的 Container 才會運行起來。

接下來看一個 Init 失敗的例子。這裡設定要存取 Service,但暫時不建立 Service,就會在 Initialize 階段一直失敗,類似 Web 應用需要依賴 Database 的場景:
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]
因為 Init Container 無法成功,Pod 會一直無法啟動:

加上對應的 Service 後:
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
  name: mydb
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9377
隨著 Service 建立起來,Initialize 成功後,Pod 就成功建立了:

我們來實戰日誌收集的 Sidecar 模式。首先建立一個沒有 Sidecar 的 Deployment:
# 建立 Deployment
kubectl create deployment game2048-logging --image=naketa/2048game
# 建立 Service (因為我是 KinD 所以沒特別設定要用 Node Port)
kubectl expose deployment game2048-logging --port=80
# Port Forward (雲端防火牆開放 Port 30123)
kubectl port-forward --address 0.0.0.0 services/game2048-logging 30123:80
2048 遊戲成功運行:


使用 kubectl logs 查看時,發現沒有任何輸出,因為這個 Image 沒有做標準輸出的導出:
kubectl logs game2048-logging-764746bb59-w7qgr

但進入 Container 可以看到日誌檔案確實存在:
kubectl exec -it game2048-logging-764746bb59-w7qgr -- /bin/sh -c "tail /var/log/nginx/access.log"

現在加上 Sidecar Container,使用共享 Volume 來收集日誌:
kubectl create deployment game2048-logging --image=naketa/2048game --dry-run=client -o yaml > game2048-sidecar.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: game2048-logging
  name: game2048-logging
spec:
  replicas: 1
  selector:
    matchLabels:
      app: game2048-logging
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: game2048-logging
    spec:
      containers:
      - image: naketa/2048game
        name: 2048game-logging
        imagePullPolicy: Always
        volumeMounts:
        - mountPath: /var/log/nginx
          name: shared-log
      - image: busybox:1.28
        name: busybox-logging
        args:
        - /bin/sh
        - -c
        - tail -fn+1 /var/log/nginx/access.log
        imagePullPolicy: Always
        volumeMounts:
        - mountPath: /var/log/nginx
          name: shared-log
      volumes:
      - emptyDir: {}
        name: shared-log
status: {}
部署後可以看到 Pod 裡面現在有兩個 Container Ready:

現在可以透過 Sidecar Container 查看日誌了 (使用 -c 指定容器名稱):
kubectl logs game2048-logging-77c447b96c-vw25q -c busybox-logging

接下來實戰 Adapter 模式,將 Nginx 的狀態透過 Adapter Container 轉換成 Prometheus 格式的 Metrics:
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx-adapter
  name: nginx-adapter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-adapter
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx-adapter
    spec:
      containers:
      - image: nigelpoulton/nginxadapter:1.0
        name: nginxadapter
      - name: transformer
        image: nginx/nginx-prometheus-exporter
        args: ["-nginx.scrape-uri","http://localhost/nginx_status"]
        ports:
        - containerPort: 9113
status: {}
建立 Service 並 Port Forward:
# 將 Deployment 的 9113 Port 對應 Service 的 80 Port
kubectl expose deployment nginx-adapter --port 80 --target-port 9113
# Port Forward (雲端防火牆開放 Port 30123)
kubectl port-forward --address 0.0.0.0 services/nginx-adapter 30123:80
成功將 Nginx 的 Metrics 轉換成 Prometheus 格式:


如果沒有使用 Adapter Container:
/nginx_status 頁面,輸出的是純文字格式的狀態資訊透過 Adapter Container,我們完全不需要修改原本的 Nginx Image,就能優雅地提供 Prometheus 相容的監控端點。
最後實戰 Ambassador 模式,建立一個主 Container 和 Ambassador Container 來處理外部 API 連線:
apiVersion: v1
kind: Pod
metadata:
  name: themoviedb
  labels:
    app: themoviedb
spec:
  containers:
    - name: main
      image: nginx
    - name: ambassador
      image: startkubernetes/ambassador:0.1.0
      env:
        - name: API_KEY
          valueFrom:
            secretKeyRef:
              name: themoviedb
              key: apikey
      ports:
        - name: http
          containerPort: 8080
建立 Secret 來存放 API Key(需要自行申請 TMDB API Key):
apiVersion: v1
kind: Secret
metadata:
  name: themoviedb
data:
  apikey: <base64-encoded-api-key>  # 需要將你的 API key 進行 base64 編碼

建立完成後,主 Container 可以透過 localhost 來存取外部 API:
kubectl exec -it themoviedb -- curl localhost:8080/movies | jq
成功取得電影資料:

如果沒有使用 Ambassador Container:
透過 Ambassador Container,主應用只需要知道 localhost:8080,所有複雜的外部連線邏輯都被封裝在 Ambassador 中,實現了完美的解耦。
今天我們深入探討了 Kubernetes Pod 的進階設計模式,從 Init Container 到 Multi-Container 的三大模式(Sidecar、Adapter、Ambassador),並透過實戰來理解每種模式的應用場景:
這些設計模式都體現了 Kubernetes 的核心理念,透過將不同職責分配給不同的容器,我們能夠建構出更靈活、可維護的應用架構。然而,隨著應用架構變得越來越複雜,手動編寫和管理這些 YAML 檔案也變得越來越困難。想像一下,如果每個環境(開發、測試、生產)都需要不同的配置,或是需要管理數十個相互依賴的服務,純手工維護 YAML 檔案將會是一場噩夢。
這就是為什麼我們需要 Helm —— Kubernetes 的套件管理工具。明天我們將深入研究 Helm,看看它如何透過 Chart,將複雜的 Kubernetes 應用打包成可重複使用的套件,並提供版本控制、依賴管理和配置管理等強大功能。